﻿using FastIM;
using FreeRedis;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Net.WebSockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Channels;

/// <summary>
/// im 核心类实现
/// </summary>
public class ImClient
{
    protected RedisClient _redis;
    protected IPubsub _pubsub;//发布订阅服务
    protected IServerDiscover _serverDiscover;//集群服务器探索者
    protected string _redisPrefix;
    protected string _pathMatch;
    public ImClientOptions ClientOptions { get; set; }
    /// <summary>
    /// 推送消息的事件，可审查推向哪个Server节点
    /// </summary>
    public EventHandler<ImSendEventArgs> OnSend;

    /// <summary>
    /// 初始化 imclient
    /// </summary>
    /// <param name="options"></param>
    public ImClient(ImClientOptions options)
    {
        if (options.Redis == null) throw new ArgumentException("ImClientOptions.Redis 参数不能为空");
        if (options.Servers.Any() == false) throw new ArgumentException("ImClientOptions.Servers 参数不能为空");
        _redis = options.Redis;
        _redisPrefix = $"wsim{options.PathMatch.Replace('/', '_')}";
        _pathMatch = options.PathMatch ?? "/ws";
        _pubsub = options.Pubsub != null ? options.Pubsub : new RedisPubsub(_redis);//设置发布订阅服务
        _serverDiscover = options.ServerDiscover != null ? options.ServerDiscover : new HashServerDiscover();//
        ClientOptions = options;
        Helper.IgnoreNullValue = options.IgnoreNullValue;
        Helper.SecretKey = options.TokenSecretKey;
    }

    /// <summary>
    /// 负载分区规则
    /// </summary>
    /// <param name="clientId">客户端id</param>
    /// <returns></returns>
    protected string SelectServer(string clientId)
    {
        return _serverDiscover.SelectServer(ClientOptions, clientId);
    }

    /// <summary>
    /// ImServer 连接前的负载、授权，返回 ws 目标地址，使用该地址连接 websocket 服务端
    /// </summary>
    /// <param name="clientId">客户端id</param>
    /// <param name="clientMetaData">客户端相关信息，比如ip</param>
    /// <returns>websocket 地址：ws://xxxx/ws?token=xxx</returns>
    public string PrevConnectServer(string clientId, ClientMetaData clientMetaData)
    {
        var server = SelectServer(clientId);
        //var token = $"{Guid.NewGuid()}{Guid.NewGuid()}{Guid.NewGuid()}{Guid.NewGuid()}".Replace("-", "");
        var token = Helper.EncodeToken(clientId, clientMetaData);
        //_redis.Set($"{_redisPrefix}Token{token}", Helper.SerializeObject((clientId, clientMetaData)), 10);
        //return $"ws://{server}{_pathMatch}?token={token}";
        return $"{server}{_pathMatch}?token={token}";
    }
    /// <summary>
    /// 根据token获取用户的登录信息，可以据此判断客户端是否有发送消息的权限
    /// </summary>
    /// <param name="token"></param>
    /// <returns></returns>
    /// <exception cref="Exception"></exception>
    public (string? clientId, ClientMetaData? clientMetaData) GetLoginInfo(string token)
    {
        //从缓存获取token值      
        //var token_value = _redis.Get($"{_redisPrefix}Token{token}");
        if (string.IsNullOrEmpty(token))
            return (null, null);
        //客户端登录信息
        var logininfo = Helper.DecodeToken(token);
        //客户端登录设备名称。用于一个设备端只能连接一个websocket
        return logininfo;
    }
    /// <summary>
    /// 向指定的多个客户端id发送消息(可以文本化的消息)
    /// </summary>
    /// <param name="senderClientId">发送者的客户端id</param>
    /// <param name="receiveClientIds">接收者的客户端id</param>
    /// <param name="message">发送的具体消息内容，不包括发送者接收者等相关信息</param>
    /// <param name="receipt">是否回执</param>
    public async Task SendMessage(string senderClientId, IEnumerable<string> receiveClientIds, object message, QualityOfServiceLevel qos = QualityOfServiceLevel.AtMostOnce, string channel = null, ClientMetaData? senderMetaData = null, string MsgId = null, int Code = 0, MsgType type = MsgType.PublishOne2One)
    {
        receiveClientIds = receiveClientIds.Distinct().ToArray();
        //待发布的聊天服务器对应的接收者信息
        Dictionary<string, ImSendEventArgs> redata = new Dictionary<string, ImSendEventArgs>();
        //判断目标用户是否在当前WebSocket服务器.如果是客户端，则无
        var wf = ImSessionFactory.Instance();
        foreach (var receiveClientId in receiveClientIds)
        {
            string receiveserver = SelectServer(receiveClientId);//根据客户端id查找对应的聊天服务器
            if (wf.Get(receiveClientId, out var deviceSockets))//接收者在本机则本机im服务器直接发送
            {
                await SendDeviceSocketsMessage(receiveserver, senderClientId, receiveClientId, deviceSockets.Values, message, qos, channel, senderMetaData,MsgId,Code,type);
            }
            else//不在本机则添加到待发布的聊天服务器待发列表
            {
                if (redata.ContainsKey(receiveserver) == false) redata.Add(receiveserver, new ImSendEventArgs(receiveserver, senderClientId, message, qos, channel, senderMetaData, MsgId,Code,type));
                redata[receiveserver].ReceiveClientId.Add(receiveClientId);
            }
        }
        //循环不在本机的发送列表
        //var messageJson = Helper.SerializeObject(message);
        foreach (var sendArgs in redata.Values)
        {
            //OnSend?.Invoke(this, sendArgs);
            //这里发布到客户端对应的服务器，由服务器订阅然后查找客户端发送
            _pubsub.Publish($"{_redisPrefix}Server{sendArgs.Server}", Helper.SerializeObject(sendArgs));
            //或者如果不在当前服务器，则直接把消息发布到具体的用户频道去，由具体用户去订阅？？
        }
    }
    /// <summary>
    /// 获取所在线客户端id
    /// </summary>
    /// <returns></returns>
    public IEnumerable<string> GetClientListByOnline()
    {
        //return _redis.HKeys($"{_redisPrefix}Online").Select(a => Guid.TryParse(a, out var tryguid) ? tryguid : Guid.Empty).Where(a => a != Guid.Empty);
        return _redis.HKeys($"{_redisPrefix}Online").Where(a => a != String.Empty);
    }

    /// <summary>
    /// 判断客户端是否在线
    /// </summary>
    /// <param name="clientId"></param>
    /// <returns></returns>
    public bool HasOnline(string clientId)
    {
        return _redis.HGet<int>($"{_redisPrefix}Online", clientId) > 0;
    }

    /// <summary>
    /// 事件订阅,上下线
    /// </summary>
    /// <param name="online">上线</param>
    /// <param name="offline">下线</param>
    public void EventBus(
        Action<(string clientId, ClientMetaData clientMetaData)> online,
        Action<(string clientId, ClientMetaData clientMetaData)> offline)
    {
        var chanOnline = $"evt_{_redisPrefix}Online";
        var chanOffline = $"evt_{_redisPrefix}Offline";
        _pubsub.Subscribe(new[] { chanOnline, chanOffline }, (chan, msg) =>
        {
            if (chan == chanOnline) online(JsonConvert.DeserializeObject<(string clientId, ClientMetaData clientMetaData)>(msg as string));
            if (chan == chanOffline) offline(JsonConvert.DeserializeObject<(string clientId, ClientMetaData clientMetaData)>(msg as string));
        });
    }
    /// <summary> 
    /// 事件订阅，加入离开频道
    /// </summary>
    /// <param name="joinchannel">加入频道</param>
    /// <param name="leavechannel">离开频道 </param>
    public void EventChan(
    Action<(string clientId, string chan)> joinchannel,
    Action<(string clientId, string chan)> leavechannel)
    {
        var chanJoinChan = $"evt_{_redisPrefix}JoinChan";
        var chanLeaveChan = $"evt_{_redisPrefix}LeaveChan";
        _pubsub.Subscribe(new[] { chanJoinChan, chanLeaveChan }, (chan, msg) =>
        {
            if (chan == chanJoinChan) joinchannel(JsonConvert.DeserializeObject<(string clientId, string chan)>(msg as string));
            if (chan == chanLeaveChan) leavechannel(JsonConvert.DeserializeObject<(string clientId, string chan)>(msg as string));
        });
    }
    #region 群聊频道，每次上线都必须重新加入

    /// <summary>
    /// 加入群聊频道，每次上线都必须重新加入
    /// </summary>
    /// <param name="clientId">客户端id</param>
    /// <param name="chan">群聊频道名</param>
    public void JoinChan(string clientId, string chan)
    {
        using (var pipe = _redis.StartPipe())
        {
            pipe.HSet($"{_redisPrefix}Chan{chan}", clientId.ToString(), 0);
            pipe.HSet($"{_redisPrefix}Client{clientId}", chan, 0);
            pipe.HIncrBy($"{_redisPrefix}ListChan", chan, 1);
            pipe.EndPipe();
        }
        //加入频道事件
        _pubsub.Publish($"evt_{_redisPrefix}JoinChan", Helper.SerializeObject((clientId, chan)));
    }
    /// <summary>
    /// 离开群聊频道
    /// </summary>
    /// <param name="clientId">客户端id</param>
    /// <param name="chans">群聊频道名</param>
    public void LeaveChan(string clientId, params string[] chans)
    {
        if (chans?.Any() != true) return;
        using (var pipe = _redis.StartPipe())
        {
            foreach (var chan in chans)
            {
                pipe.HDel($"{_redisPrefix}Chan{chan}", clientId.ToString());
                pipe.HDel($"{_redisPrefix}Client{clientId}", chan);
                pipe.Eval($"if redis.call('HINCRBY', KEYS[1], '{chan}', '-1') <= 0 then redis.call('HDEL', KEYS[1], '{chan}') end return 1", new[] { $"{_redisPrefix}ListChan" });
                //离开频道事件
                _pubsub.Publish($"evt_{_redisPrefix}LeaveChan", Helper.SerializeObject((clientId, chan)));
            }
            pipe.EndPipe();
        }
    }
    /// <summary>
    /// 获取群聊频道所有客户端id
    /// </summary>
    /// <param name="chan">群聊频道名</param>
    /// <returns></returns>
    public string[] GetChanClientList(string chan)
    {
        //return _redis.HKeys($"{_redisPrefix}Chan{chan}").Select(a => Guid.Parse(a)).ToArray();
        //return _redis.HKeys($"{_redisPrefix}Chan{chan}").Select(a => a).ToArray();
        return _redis.HKeys($"{_redisPrefix}Chan{chan}").ToArray();
    }
    /// <summary>
    /// 清理群聊频道的离线客户端（测试）
    /// </summary>
    /// <param name="chan">群聊频道名</param>
    public void ClearChanClient(string chan)
    {
        var websocketIds = _redis.HKeys($"{_redisPrefix}Chan{chan}");
        var offline = new List<string>();
        var span = websocketIds.AsSpan();
        var start = span.Length;
        while (start > 0)
        {
            start = start - 10;
            var length = 10;
            if (start < 0)
            {
                length = start + 10;
                start = 0;
            }
            var slice = span.Slice(start, length);
            var hvals = _redis.HMGet($"{_redisPrefix}Online", slice.ToArray().Select(b => b.ToString()).ToArray());
            for (var a = length - 1; a >= 0; a--)
            {
                if (string.IsNullOrEmpty(hvals[a]))
                {
                    offline.Add(span[start + a]);
                    span[start + a] = null;
                }
            }
        }
        //删除离线订阅
        if (offline.Any()) _redis.HDel($"{_redisPrefix}Chan{chan}", offline.ToArray());
    }

    /// <summary>
    /// 获取所有群聊频道和在线人数
    /// </summary>
    /// <returns>频道名和在线人数</returns>
    public IEnumerable<(string chan, long online)> GetChanList()
    {
        var ret = _redis.HGetAll<long>($"{_redisPrefix}ListChan");
        return ret.Select(a => (a.Key, a.Value));
    }
    /// <summary>
    /// 获取用户参与的所有群聊频道
    /// </summary>
    /// <param name="clientId">客户端id</param>
    /// <returns></returns>
    public string[] GetChanListByClientId(string clientId)
    {
        return _redis.HKeys($"{_redisPrefix}Client{clientId}");
    }
    /// <summary>
    /// 获取群聊频道的在线人数
    /// </summary>
    /// <param name="chan">群聊频道名</param>
    /// <returns>在线人数</returns>
    public long GetChanOnline(string chan)
    {
        return _redis.HGet<long>($"{_redisPrefix}ListChan", chan);
        //return _redis.HGet<long>($"{_redisPrefix}ListChan", chan);
        //return Convert.ToInt64(_redis.HGet($"{_redisPrefix}ListChan", chan));//不行       
    }

    /// <summary>
    /// 发送群聊消息，所有在线的用户将收到消息
    /// </summary>
    /// <param name="senderClientId">发送者的客户端id</param>
    /// <param name="chan">群聊频道名</param>
    /// <param name="message">消息</param>
    public async Task SendChanMessage(string senderClientId, string chan, object message, ClientMetaData? senderMetaData = null, string MsgId = null, int Code = 0)
    {
        var websocketIds = _redis.HKeys($"{_redisPrefix}Chan{chan}");
        //SendMessage(Guid.Empty, websocketIds.Where(a => !string.IsNullOrEmpty(a)).Select(a => Guid.TryParse(a, out var tryuuid) ? tryuuid : Guid.Empty).ToArray(), message);     
        await SendMessage(senderClientId, websocketIds.Where(a => !string.IsNullOrEmpty(a)).Select(a => a).ToArray(), message, QualityOfServiceLevel.AtMostOnce, chan, senderMetaData,MsgId,Code,MsgType.Publish);
    }
    /// <summary>
    /// 向指定用户receiveClientId的多端设备deviceSockets发送消息
    /// </summary>
    /// <param name="receiveServer">接收者服务器，可选，用于websocket发送成功后回调事件信息</param>
    /// <param name="senderClientId"></param>
    /// <param name="receiveClientId"></param>
    /// <param name="deviceSockets"></param>
    /// <param name="message"></param>
    /// <param name="receipt"></param>
    /// <param name="channel"></param>
    /// <returns></returns>
    protected async Task SendDeviceSocketsMessage(string? receiveServer, string senderClientId, string receiveClientId, ICollection<ImSession> deviceSockets, object message, QualityOfServiceLevel qos = 0, string? channel = null, ClientMetaData? senderMetaData = null,string MsgId=null, int Code=0, MsgType type=MsgType.PublishOne2One)
    {
        //如果接收消息人是发送者，并且接收者只有1个以下设备登录，则不发送
        //只有接收者为多端时，才转发消息通知其他端 
        if (receiveClientId == senderClientId && deviceSockets.Count <= 1) return; //注释掉表示自己能给自己发信息
        //构建客户端接收到的消息体.接收消息体不必带有ReceiveClientId 
        var msgBody = new ResponseMsgBody { SenderClientId = senderClientId, Payload = message, Channel = channel, SenderMetaData = senderMetaData,MsgId= MsgId, Code=Code };
        var msgTemp = new MsgTemplate {Type=type, Body = msgBody };
        var sendMsg = Helper.SerializeObject(msgTemp);
        byte[] sendByte = Encoding.UTF8.GetBytes(sendMsg);
        foreach (var targetSocket in deviceSockets)
        {
            if (targetSocket.socket.State == WebSocketState.Open)
            {
                await targetSocket.socket.SendAsync(new ArraySegment<byte>(sendByte, 0, sendByte.Length), WebSocketMessageType.Text, true, CancellationToken.None);
                var sendArgs = new ImSendEventArgs(receiveServer, senderClientId, message, qos, channel,senderMetaData, MsgId,Code,type);
                sendArgs.ReceiveClientId.Add(receiveClientId);
                OnSend?.Invoke(this, sendArgs);//服务器发送成功回调事件
            }
        }
    }
    /// <summary>
    /// 发送全员消息
    /// </summary>
    /// <param name="message"></param>
    public void SendAllMessage(string senderClientId, object message, ClientMetaData? senderMetaData = null,string MsgId=null)
    {
        foreach (var server in ClientOptions.Servers)
        {
            var sendArgs = new ImSendEventArgs(null, senderClientId, message, QualityOfServiceLevel.AtMostOnce, "all", senderMetaData,MsgId);
            //这里发布到server对应的服务器，由服务器订阅然后查找客户端发送
            _pubsub.Publish($"{_redisPrefix}Server{server}", Helper.SerializeObject(sendArgs));
        }
    }
    #endregion
}
